http://www.cs.mcgill.ca/~cs251/OldCourses/1997/topic25/

Ko delamo z grafi, je pogosto koristno, ce lahko na nek
sistematicen nacin preiscemo ves graf (oz. vsaj vse tisto,
kar je dosegljivo iz dolocene tocke).  (Rekli bomo, da je
tocka v dosegljiva iz tocke u, ce obstaja neka pot od u
do v.  Pri usmerjenem grafu mora pot seveda tudi spostovati
smer puscic na povezavah.)

Splosna zamisel tu opisanih postopkov za preiskovanje grafov je
naslednja: vzdrzujmo neko mnozico tock, ki jih bo se treba obiskati.
Vsakic vzemimo po eno tocko iz te mnozice in jo "obiscimo".  Slednje
pomeni, da pregledamo vse njene sosede; tiste, ki jih se nismo obiskali,
dodajmo v tisto mnozico tock, ki jih bo se treba obiskati (seveda so
mogoce nekatere ze v tej mnozici, ce smo jih zagledali ze pri obisku
kaksne prejsnje tocke).  

Vprasanje pa je, kako si izberemo, katero tocko iz tiste mnozice
bomo res obiskali v naslednjem koraku.  Dva znacilna odgovora nas
pripeljeta do dveh znanih postopkov pregledovanja grafov: iskanje
v sirino in iskanje v globino.

--

Iskanje v sirino (breadth-first search)
http://www.cs.mcgill.ca/~cs251/OldCourses/1997/topic27/
[tale URL, pa tudi tisti spodaj pri iskanju v globino, je zelo
priporocljiv, ker se tu najdejo slike in animacije, ki kazejo,
kako ta dva postopka preiskujeta graf]

Tu si mislimo, da je mnozica tock, ki jih bo se treba obiskati,
v resnici vrsta.  To pomeni, da imamo pravzaprav seznam in tocke
dodajamo vedno na konec seznama, ko si je treba izbrati novo tocko,
da jo bomo obiskali, pa jo vedno vzamemo z zacetka seznama.

Postopek bi bil zdaj tak:

Vhod: graf in neka zacetna tocka s;
1. Q := [prazna vrsta]
2. dodaj s na konec vrste Q
3. dokler Q ni prazna:
4.     vzemi prvo tocko (recimo u) iz vrste;
5.     za vsako u-jevo sosedo (ali pa naslednico, ce 
       je graf usmerjen), recimo v:
6.         ce v se nikoli nismo dodali v vrsto,
7.             dodaj v na konec vrste;

Da lahko v vrstici (6) enostavno preverimo, ce smo v ze kdaj dodali
v vrsto, je seveda koristno neko tabelo, npr. 
ZeVidena: array[1..nTock] of Boolean, v kateri na zacetku postavimo
vse elemente na False, nato pa v vrsticah (2) in (7) postavimo
ustrezni element na True.

Razmislimo, kako ta postopek preiskuje graf.  Zaceli bi pri tocki 
s in jo dodali v vrsto.  Nato bi jo vzeli iz vrste in si ogledali
vse njene sosede; ker se nobena ni bila v vrsti, bi zdaj dodali
v vrsto vse s-jeve sosede.  

V nadaljevanju bi jemali te s-jeve sosede eno po eno iz vrste in 
pri vsaki pogledali _njene_ sosede; med slednjimi je lahko s ali 
pa katera od s-jevih sosed, lahko pa je tudi kaksna tocka, ki je 
doslej se nismo videli in v tem primeru bomo to zdaj tudi dodali v 
vrsto.  

Ko bi tako iz vrste scasoma pobrali vse s-jeve sosede, bi prisle
na vrsto tocke, ki sicer niso s-jeve sosede, pac pa smo jih dodali v
vrsto takrat, ko smo obiskovali s-jeve sosede.  Itd.

Skratka, vidimo lahko, da ta algoritem preiskuje graf nekako po
plasteh: najprej se v vrsti znajde sama tocka s; nato vse tiste, do
katerih lahko pridemo iz s v enem koraku (= s-jeve sosede); nato
vse tiste, do katerih iz s sicer ne moremo priti v enem koraku, lahko
pa v dveh; nato vse, do katerih gre s tremi koraki, ne pa z manj; itd.
Sele ko obiscemo vse tocke ene plasti, se posvetimo naslednji plasti.
Zato pravijo, da gre ta algoritem najprej v sirino in sele potem v 
globino: najprej obisce eno plast v celoti, sele nato gre na 
naslednjo ("globljo") plast.  Od tod izraz "breadth-first search" 
(BFS) oz. iskanje v sirino.

Ko dodamo v vrstici (7) neko tocko "v" v vrsto, bi si lahko tudi
zapomnili, kaksen je bil tisti hip "u".  Recimo temu podatku p[v]
(predhodnik v-ja pri preiskovanju grafa).  Ce bi zdaj gledali 
zaporedje v, p[v], p[p[v]], p[p[p[v]]], ..., bi scasoma prisli
nazaj do s-ja.  Tako smo torej nasli neko pot od s do v; se vec,
ta pot je celo najkrajsa (najdaljsa v smislu stevila korakov -- 
se pravi, kot da bi predpostavili, da je pri poti pomembno le to,
iz koliko povezav je sestavljena, povezave pa imamo vse za 
enakovredne; ce bi hoteli ravnati tako, kot da so tudi povezave
razlicno dolge, bi morali uporabiti kak drug postopek).  To sledi
iz nacina, kako BFS preiskuje graf.  Vse, do cesar se da priti v
enem samem koraku, bo tudi res nasel v enem koraku; za vse, do cesar
se da priti v dveh, ne pa v enem, bo res nasel poti, dolge dva
koraka; itd., skratka, do vsake tocke res najde najkrajso mozno
pot.

Tudi te dolzine najkrajsih poti od s do ostalih tock bi lahko
racunali sproti.  Na zacetku bi vpisali d(s, s) := 0 in nato
v vrstici (7) dodali d(s, v) := d(s, u) + 1.

Ker obisce BFS vse tocke, dosegljive iz s, ga lahko uporabimo za
ugotavljanje, kaj vse je dosegljivo iz s.  Se ena posledica tega 
preiskovanja po plasteh oz. v sirino je ta, da vedno velja 
naslednje: prvih nekaj tock v vrsti je na neki razdalji (recimo 
k korakov) od zacetne tocke s, ostale tocke pa so na razdalji k+1.
Zato lahko, ce bi recimo hoteli obiskati vse tocke, ki so 
dosegljive iz s po najvec K korakih, to z iskanjem v sirino 
dosezemo zelo preprosto: zapomnimo si, kje v vrsti pride do 
prehoda od k na k+1; ko pridemo do prehoda in je k+1 = K, vemo, 
da smo doslej obiskali ze vse tocke, dosegljive v K-1 ali manj 
korakih, v vrsti pa imamo zdaj ravno se vse tiste, ki so 
dosegljive v tocno K korakih (ne pa v manj korakih).

Uporaben je tudi za iskanje povezanih komponent v neusmerjenih 
grafih.  Spomnimo se, da je povezana komponenta taka skupina 
tock, da je vsaka tockaskupine dosegljiva iz vsake druge, obenem 
pa v skupino ne moremo dodati nobene nove tocke, ne da bi ta pogoj 
prenehal veljati.  No, ce zacnemo kar v poljubni tocki s in 
pozenemo iz nje BFS, bomo obiskali ravno vse tocke iz njene 
povezane komponente.  (Zakaj? Recimo, da je neka tocka v s-jevi 
povezani komponenti; torej je dosegljiva iz s; torej jo bo nas BFS 
obiskal.  Zdaj pa recimo, da neka tocka u ni v s-jevi povezani 
komponenti, nas BFS (ki zacne  pri s) pa bi jo vendarle obiskal; 
toda ker jo je obiskal, je  u dosegljiva iz s; in ker je graf 
neusmerjen, je tudi s dosegljiva iz u; torej so tudi vse iz s-jeve 
povezane komponente dosegljive iz u (namrec prek s), ta pa iz njih;
torej u v resnici spada v s-jevo povezano komponento, kar je 
protislovje.  S tema dvema razmislekoma smo se prepricali, da bo 
BFS obiskal vse tocke iz s-jeve povezane komponente, pa nobene 
druge tocke.)  Potem pa lahko isti postopek pozenemo znova, le 
da za "s" vzamemo kaksno od tock, ki jih doslej se nismo obiskali.
S tem bomo nasli se eno povezano komponento.  Tako nadaljujemo, 
dokler ne obiscemo vseh tock.

procedure PovezaneKomponente;
vhod: neusmerjen graf G z n tockami;
izhod: Komponenta: array[1..n] of Integer;
       (* Komponenta[u] pove, kateri komponenti pripada tocka u *)
       StKomponent: Integer;
var s, v, u: 1..n; Q: vrsta;
begin
 1  StKomponent := 0;
 2  for u := 1 to n do Komponenta[u] := 0;
 3  for s := 1 to n do if Komponenta[s] = 0 then
 4      StKomponent := StKomponent + 1;
 5      Q := prazna vrsta;
 6      dodaj s v vrsto; Komponenta[s] := StKomponent;
 7      dokler Q ni prazna:
 8          vzemi neko tocko, recimo ji u, z zacetka vrste Q;
 9          za vsako u-jevo sosedo, recimo v:
10              if Komponenta[v] = 0 then
11                  dodaj "v" v vrsto; Komponenta[v] := StKomponent;
end;

Tukaj tabelo Komponenta obenem izkoriscamo se za hranjenje
podatka o tem, ce je neka tocka trenutno ze v vrsti -- ce je,
je Komponenta[u] ze razlicna od 0.

--

Se beseda o implementaciji vrste.  Vrsto lahko implementiramo 
na tradicionalni nacin, s seznamom, lahko pa tudi s tabelo
in dvema indeksoma (za zacetek in konec vrste).  Na primer:

var Q: array[1..N] of 1..N; 
 InQ: array[1..N] of Boolean;
 Head, Tail: Integer;
 U, V: Integer;
begin
 (* Pripravimo prazno vrsto *)
 Head := 1; Tail := 1;
 for U := 1 to N do InQ[U] := False;
 (* Dodajmo zacetno tocko, S, v vrsto. *)
 Q[Tail] := S; Tail := Tail + 1;
 while Head < Tail do
  begin
  (* Vzemimo neko tocko, U, z zacetka vrste. *)
  U := Q[Head]; Head := Head + 1;
  (* Preglejmo U-jeve sosede. *)
  for V .... do (* po vseh U-jevih sosedih *)
   (* Ce sosede V se nismo nikoli dodali v vrsto,
      jo dodajmo zdaj. *)
   if not InQ[V] then
    begin
    Q[Tail] := V; Tail := Tail + 1;
    InQ[Tail] := True;
    end;
  end;
end;

(V zanki "for V" sem dodal pikice zato, ker bo izvedba te zanke 
odvisna od tega, ali delamo z matriko sosednosti ali pa s
seznamom sosedov (oz. naslednikov, ce je graf usmerjen).)

V seznamom pa bi bilo mogoce takole:

type PRec = ^TRec;
  TRec = record U: 1..N; Next: PRec; end;
var Head, Tail: PRec;
 InQ: array[1..N] of Boolean;
 U, V: Integer;

 (* doda U na konec vrste *)
 function Enqueue(U: Integer): PRec;
 var P: PRec;
 begin
  New(P); P^.U := U; P^.Next := nil;
  if Tail = nil then begin Head := P; Tail := P; end
  else begin Tail^.Next := P; Tail := P; end;
  InQ[U] := True; Enqueue := P;
 end;

 (* vrne vozlisce z zacetka vrste *) 
 function Dequeue: Integer;
 var P: PRec;
 begin
  P := Head; Dequeue := P^.U;
  Head := P^.Next; if Head = nil then Tail := nil;
  Dispose(P); 
 end;

begin
 Head := nil; Tail := nil;
 for U := 1 to N do InQ[U] := False;
 Enqueue(s);
 while Head <> nil do
  begin 
  U := Dequeue;
  for V .... do (* po vseh U-jevih sosedih *)
   if not InQ[V] then Enqueue(V);
  end;
end.

Ce primerjamo izvedbo vrste s seznamom in s tabelo, vidimo, da
ima slednja malo manj knjigovodskega dela, zato pa ves cas zre
pomnilnika za N celic, medtem ko porabimo za seznam samo toliko
celic, kolikor je v vsakem trenutku tock v vrsti.  Je pa pri
iskanju v sirino res, da bomo lahko imeli scasoma v vrsti kar
veliko tock (ker gremo pac najprej v sirino in sele potem v
globino -- v dolocenem trenutku v bistvu hranimo v vrsti eno celo
plast), zato to tudi ni tako huda razlika.

[P.S.  Jaz pravzaprav ze ves cas predpostavljam, da vam seznami
(linked lists) ne delajo nobenih tezav.  Upam, da to drzi.  Ce
komu kaj ni jasno, povejte, pa bom pripravil se en mail o 
seznamih.]

--

Iskanje v globino (depth-first search)
http://www.cs.mcgill.ca/~cs251/OldCourses/1997/topic26/

Pri iskanju v sirino smo, ko smo obiskali neko vozlisce in si
ogledali, katere sosede ima, te sosede (ce jih nismo opazili ze
kdaj prej) dodali na konec seznama (oz. vrste).  Preden obiscemo
taksne sosede, bi obiskali se vse drugo, kar je bilo tisti hip v
vrsti pred njimi.

Druga moznost pa je, da taksne na novo opazene sosede obiscemo
takoj, ko jih zagledamo.  To je tako, kot bi na novo opazena
vozlisca namesto na konec seznama dodajali na zacetek, torej
prav tja, od koder tudi beremo, ko se odlocamo, katero vozlisce
bi obiskali naslednjic.  Z drugimi besedami, namesto vrste (kot
pri iskanju v sirino) bi tukaj uporabili sklad.  Dobljeni
postopek imenujmo "iskanje v globino".

Vhod: graf in neka zacetna tocka s;
1. S := [praznen sklad]
2. dodaj s na vrh sklada S
3. dokler S ni prazen:
4.     vzemi prvo tocko (recimo u) z vrha sklada;
5.     za vsako u-jevo sosedo (ali pa naslednico, ce 
       je graf usmerjen), recimo v:
6.         ce v se nikoli nismo dodali v sklad,
7.             dodaj v na vrh sklada;

Vidimo, da je cisto podoben iskanju v sirino, le namesto vrste
uporablja sklad.  No, pogosto bi ga implementirali tudi takole:

var ZeObiskana: array[1..n] of Boolean;

procedure DepthFirstSearch(u: 1..n);
1. ZeObiskana[u] := True;
2. za vsako u-jevo sosedo (oz. naslednico, ce 
   je graf usmerjen), recimo v:
3.      if not ZeObiskana[v] then
4.          DepthFirstSearch(v);

Seveda bi morali pred prvim klicem tega podprograma postaviti
ZeObiskana[u] := False za vse u.  Razlika med tem in gornjim
podprogramom je le v tem, da tale drugi obisce u-jeve naslednike
v takem vrstnem redu, kot jih najde, gornji pa v nasprotnem
vrstnem redu (ker najprej obisce tistega, ki ga zadnjega doda
na sklad), vendar je ta razlika obicajno cisto nepomembna.
Druga razlika je se ta, da nam tule ni treba eksplicitno
delati s skladom -- delo s skladom tule pravzaprav prelozimo na
prevajalnik, saj ta uporablja sklad za hranjenje parametrov in
lokalnih spremenljivk pri gnezdenih klicih podprogramov, pri nas pa 
ravno prek tega dvojega (parametra u in spremenljivke "v" v zanki
v vrstici (2)) vzdrzujemo vse informacije o tem, kaj bo treba se 
obiskati.

Za DFS velja podobno kot zgoraj za BFS -- ce ga poklicemo z
neko zacetno tocko s (torej DepthFirstSearch(s)), bo scasoma
obiskal vse tocke, dosegljive iz s.  (Zato bi lahko tudi DFS 
uporabili pri ugotavljanju povezanih komponent grafa.)  Razlika v 
primerjavi z BFS je, da se vsakic, ko pride v neko tocko u, najprej 
zakoplje v enega od njenih sosedov (oz. naslednikov, ce je graf 
usmerjen) in sele ko prebrska vse, kar je dosegljivo iz tega soseda, 
se loti naslednjega soseda ipd., sele ko so vsi sosedje preiskani,
pa sestopi nazaj iz u in se posveti se cemu drugemu.  Skratka,
vedno se zakoplje v globino, ne pa v sirino kot BFS -- od tod tudi
ime tega postopka.

Vcasih namesto tabele ZeObiskana, kot smo jo uporabili zgoraj,
govorijo tudi o "barvi" vsake tocke.  Na zacetku so vse tocke
bele; ko se klice DepthFirstSearch(u), pobarva u sivo, nato pa,
ko preneha s pregledovanjem njegovih sosedov, ga pobarva crno.
To, da je tocka siva, torej pomeni, da se trenutno ukvarjamo
s pregledovanjem njenih sosedov (oz. naslednikov, ce je graf
usmerjen), crna pa bo postala sele, ko bomo obiskali ze vse
tocke, ki so dosegljive iz nje.

var Barva: array[1..n] of (Bela, Siva, Crna);

procedure DepthFirstSearch(u: 1..n);
1. Barva[u] := Siva;
2. za vsako u-jevo sosedo (oz. naslednico, ce 
   je graf usmerjen), recimo v:
3.      if Barva[v] = Bela then
4.          DepthFirstSearch(v);
5. Barva[u] := Crna;

Recimo, da imamo v nekem trenutku na skladu taksne rekurzivne
klice:

DepthFirstSearch(123)
  DepthFirstSearch(234)
    DepthFirstSearch(345)
      DepthFirstSearch(456)
      
in da smo zdajle pri u = 456 v vrstici 3 naleteli na soseda
v = 123.  Seveda je DepthFirstSearch(123) pobarval to tocko sivo,
se preden je poklical DepthFirstSearch(234), in ker se slednji se
ni vrnil, DepthFirstSearch(123) tudi se ni utegnil pobarvati te
tocke crno.  Torej bomo mi zdajle v vrstici 3 pri u = 456 opazili,
da sosed v = 123 ni bel, pac pa je siv.  Kaj to pomeni?  Ker je
ta v zdajle siv, smo ocitno v fazi pregledovanja tock, ki so 
dosegljive iz njega -- u mora biti torej dosegljiv iz v (jasno:
iz 123 se je prislo v 234, od tam v 345, od tam pa v u = 456,
saj ravno to nam pove pogled na sklad).  Pravkar pa smo opazili
tudi v med u-jevimi sosedi, torej obstaja cikel (v nasem primeru je
to 123-234-345-456-123).  Iskanje v globino je torej tudi eden 
od nacinov, kako odkriti cikle v grafu.  Lepo pri tem je, da 
deluje tudi za neusmerjene grafe (topolosko urejanje, s katerim
se da, kot smo videli, tudi odkrivati prisotnost ciklov, pa
zahteva usmerjen graf).  Pri neusmerjenih grafih je treba paziti
le na naslednje -- ce je najdeni v siv, to lahko pomeni tudi, da
je v tisto vozlisce, iz katerega smo sploh prisli v u (pri nasem
primeru bi npr. ob pregledovanju u = 456 naleteli tudi na v = 345,
saj je to tudi u-jev sosed, obenem pa bi bil se siv, kar pa seveda
samo po sebi se ne bi pomenilo, da je v grafu cikel).  Ce torej
hocemo z DFS odkrivati cikle v neusmerjenih grafih, je koristno,
ce poleg u kot parameter prenasamo se tocko, iz katere smo do
u-ja prisli (npr. vrstica 4 bi se spremenila v DepthFirstSearch(v, u)).

--

Kar smo tule povedali o iskanju povezanih komponent v neusmerjenih
grafih, je seveda uporabno tudi za iskanje sibko povezanih komponent
v usmerjenih grafih (saj niso sibko povezane komponente ze po 
definiciji cisto nic drugega kot povezane komponente grafa, ki 
ga dobimo, ce v nasem usmerjenem grafu zanemarimo smeri povezav).
Za krepko povezane komponente v usmerjenem grafu pa obstaja nek
malo bolj zapleten algoritem, ki ga tule ne nameravam razloziti,
saj tudi se nisem opazil, da bi se ga pri nalogah z raznih tekmovanj
kdaj potrebovalo.  No, ce koga prav posebej zanima, pa povejte.

--

Ker sta BFS in DFS koristna za preverjanje dosegljivosti v grafu
in za iskanje povezanih komponent, sta seveda koristna tudi za
preverjanje povezanosti grafa.  Spomnimo se, da je neusmerjen graf
povezan, ce se da od vsake tocke priti do vsake druge.  To lahko
preverimo tako, da pozenemo BFS ali DFS v poljubni tocki in 
preverimo, ce nam je uspelo na ta nacin obiskati vse tocke v grafu;
ce nam je, je graf povezan, sicer pa ni.

--

Za konec lahko razmislimo se o casovni in prostorski zahtevnosti
opisanih algoritmov.  Iskanje v sirino doda vsako tocko v vrsto
najvec enkrat samkrat in jo zato tudi najvec enkrat vzame iz
vrste.  Ko pa jo vzame iz vrste, mora precesati vse povezave,
ki gredo iz te tocke.  Za vsako tocko si mora tudi zapomniti,
da je se ni bilo v vrsti.  Zato ima konstantno mnogo dela za 
vsako tocko nasega grafa in tudi konstantno mnogo vsako povezavo.
Casovna zahtevnost je torej O(|V| + |E|).  No, to spet predpostavi,
da imamo, ko je treba preiskati sosede neke tocke, opravka le
s tistimi tockami, ki so res njeni sosedje; torej da imamo na 
voljo sezname sosedov.  Ce bi imeli na voljo le matriko sosedov,
bi morali vedno precesati celo vrstico taksne matrike in bi bila
casovna zahtevnost zato O(|V|^2) (podobna neugodna situacija kot
pri topoloskem urejanju).

Iskanje v globino ima prav tako casovno zahtevnost kot iskanje v
sirino -- saj je tudi postopek cisto tak.  Zelo podoben razmislek
nas lahko tudi preprica, da ima tudi iskanje povezanih komponent
casovno zahtevnost O(|V| + |E|).

Kaj pa prostorska zahtevnost?  Iskanju v globino se lahko v
najslabsem primeru zgodi, da ima na skladu prakticno vse tocke
(npr. ce je graf ena sama dolga veriga, npr. povezave 1-2, 2-3,
3-4, ..., (n-1)-n), iskanju v sirino pa enako za vrsto (ce je 
graf zvezdast -- npr. povezave 1-2, 1-3, 1-4, ..., 1-(n-1), 1-n).
Torej oba v najslabsem primeru potrebujeta O(|V|) pomnilnika;
no, pravzaprav ga toliko potrebujeta ze za to, da si sploh
zapomnita, katere tocke sta ze obiskala.

--

Na valladolidskem strezniku kar mrgoli nalog, pri katerih prideta
prav BFS in/ali DFS.  Vcasih mora biti clovek malo zvit, da v
nalogi vidi graf.  Tule jih je nekaj -- vsekakor vam priporocam, 
da se lotite kaksne od njih.

336, 321, 383, 388, 439, 310, 314, 407, 253, 469, 315